/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package xenon3d;

import javax.media.opengl.DebugGL2;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.TraceGL2;
import javax.media.opengl.glu.GLU;
import xenon3d.vector.Color3f;

/**
 * The View3D object manages all parameters and settings needed for rendering a
 * three dimensional scene from one viewpoint. A view maintains a link to a
 * Canvas3D that the view is rendered into. It exists outside of the scene graph,
 * but attaches to a camera node object in the scene graph.<p>
 * The View3D object is the main object for controlling the Xenon3D viewing model.
 * All of the components that specify the view transform used to render to the
 * Canvas3D are either contained in the View3D object or in objects that are
 * referenced by it.<p>
 * The View3D object has several propertes and methods, but most are calibration
 * variables or user-helper functions. Most properties are affected by the Jogl
 * rendering thread. Therefore, the setting of most properties can only be done
 * from the Jogl rendering thread, in other words: from one of the callback
 * methods of the RenderListener interface. Settings may be retrieved from any
 * thread, though.<p>
 * Note that the rendering process automatically starts as soon as a the View3D is
 * attached to the Xenon3D object. By default, the rendering is only done
 * whenever a paint event for the canvas occours. For most 3D applications, it
 * will be more appropriate for the rendering to occour as fast as possible or
 * at fixed time intervalls. Use the start() and stop() methods to turn fast
 * rendering on or off.
 * 
 * @author Volker Everts
 * @version 0.1 - 13.08.2011: Created
 */
public class View3D {
    
    // <editor-fold defaultstate="collapsed" desc=" Private Fields ">
    
    // ----- Rendering -----

    /** The Canvas3D to which this view is attached. */
    private Canvas3D canvas;
    
    /** The internal GLEventListener object. */
    private GLListener internalListener;
    
    /** The internal frame count. */
    private int frameCount;
    
    /** The OpenGL clear flags. */
    private int clearFlags = GL2.GL_COLOR_BUFFER_BIT;

    /** The OpenGL background clear color. */
    private Color3f color;

    /** The OpenGL depth buffering flag. */
    private boolean depthBuffer;
    
    // ----- Viewport and Projection -----

    /** The viewport width. */
    private int vpWidth;

    /** The viewport height. */
    private int vpHeight;

    /** The aspect ratio. */
    private float aspect = 1.0f;

    /** The field of view. */
    private float fovy = 45.0f;

    /** The near clipping distance. */
    private float near = 0.1f;

    /** The far clipping distance. */
    private float far = 100.0f;

    /** The left clipping coordinate. */
    private float left = -1.0f;

    /** The right clipping coordinate. */
    private float right = 1.0f;

    /** The top clipping coordinate. */
    private float top = 1.0f;

    /** The bottom clipping coordinate. */
    private float bottom = -1.0f;

    /** The scale factor. */
    private float scale = 1.0f;

    // ----- Change Flags -----

    /** The view mode change flag. */
    private static final int CF_VIEW_MODE = 1;

    /** The clear color change flag. */
    private static final int CF_CLEAR_COLOR = 2;

    /** The depth buffer change flag. */
    private static final int CF_DEPTH_BUFFER = 4;

    /** The projection change flag. */
    private static final int CF_PROJECTION = 8;

    /** The change flags variable. */
    private int changeFlags = 0;

    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc=" View3D Policies ">
    
    // ----- The View3D Mode Policy -----

    /**
     * The view mode policy enumeration.
     */
    public enum ViewModePolicy {

        /** The release view mode policy, uses the default GL object. */
        Release,

        /** The debug view mode policy, uses the DebugGL object. */
        Debug,

        /** The trace view mode policy, uses the TraceGL object. */
        Trace;

    } // end enum ViewModePolicy

    /** The current ViewModePolicy, one of Release, Debug, or Trace. */
    private ViewModePolicy mode = ViewModePolicy.Release;

    /**
     * Returns the ViewModePolicy, one of Release, Debug or Trace.
     * Default: Release.
     * @return the current ViewModePolicy
     */
    public ViewModePolicy getViewModePolicy() {
        return mode;
    }

    /**
     * Sets a new ViewModePolicy for this view. Default: Release<p>
     * NOTE: this method call will enforce the new view mode for the next
     * rendered frame.
     * @param policy the new ViewModePolicy
     */
    public void setViewModePolicy(ViewModePolicy policy) {
        if (policy == null) throw new NullPointerException();
        if (policy != mode) {
            mode = policy;
            changeFlags |= CF_VIEW_MODE;
        }
    }

    // ----- The Projection Policy -----

    /**
     * The projection policy enumeration.
     */
    public enum ProjectionPolicy {

        /** The perspective projection policy. */
        Perspective,

        /** The parallel projection policy. */
        Parallel,

        /** The frustum based perspective projection policy. */
        Frustum;

    } // end enum ProjectionPolicy

    /** The current projection policy, one of Perspective, Parallel, or Frustum. */
    private ProjectionPolicy projection = ProjectionPolicy.Perspective;

    /**
     * Returns the ProjectionPolicy, one of Perspective, Parallel, or
     * Frustum.
     * @return the current ProjectionPolicy
     */
    public ProjectionPolicy getProjectionPolicy() {
        return projection;
    }

    /**
     * Sets a new ProjectionPolicy for this view.<p>
     * NOTE: this method call will force the recalculation of the current
     * projection matrix at the start of the next rendered frame.
     * @param policy the new ProjectionPolicy
     */
    public void setProjectionPolicy(ProjectionPolicy policy) {
        if (policy == null) throw new IllegalArgumentException();
        if (policy != projection) {
            projection = policy;
            changeFlags |= CF_PROJECTION;
        }
    }

    // </editor-fold>
            
    // <editor-fold defaultstate="collapsed" desc=" Initialization ">

    /**
     * Creates a new View3D with default render capabilities.
     */
    public View3D() {}
    
    /**
     * Package private method that returns the internal GLEventListener object
     * of this View3D.
     * @return the internal GLEventListener, or null, if the view is not attached
     * to a Canvas3D object
     */
    GLEventListener getInternalListener() {
        return internalListener;
    }
    
    /**
     * Package private method to set the Canvas3D object for this view.
     * @param the Canvas3D to which this view will be attached
     */
    void addCanvas(Canvas3D canvas) {
        if (canvas == null) throw new NullPointerException();
        if (this.canvas != null) throw new IllegalStateException(Xenon3D.ERR_VIEW_ALREADY_ATTACHED);
        internalListener = new GLListener(canvas);
        this.canvas = canvas;
    }
    
    /**
     * Package private method to remove the view's Canvas3D object.
     */
    void removeCanvas() {
        if (canvas == null) return;
        internalListener = null;
        canvas = null;
    }
    
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc=" Public Properties ">
    
    // ----- Rendering -----
    
    /**
     * Returns the frame number.
     * @return the number of the current frame
     */
    public int getFrameNumber() {
        return frameCount;
    }

    /**
     * Returns the background clear color.
     * @return the current background clear color
     */
    public Color3f getClearColor() {
        return color;
    }

    /**
     * Sets a new background clear color. If the new color is null, the color
     * buffer will not be cleared at the start of each frame.<p>
     * NOTE: this method call will enforce the new clear color for the next
     * rendered frame.
     * @param color the new clear color
     */
    public void setClearColor(Color3f color) {
        if (color == this.color) return;
        this.color = color;
        changeFlags |= CF_CLEAR_COLOR;
    }

    /**
     * Gets a flag indicating whether or not depth buffering is enabled for this
     * view.
     * @return true, if depth buffering is enabled
     */
    public boolean getDepthBufferEnable() {
        return depthBuffer;
    }

    /**
     * Sets a flag indicating whether or not depth buffering will be enabled for
     * this view.<p>
     * NOTE: this method call will enforce the new depth buffering setting for
     * the next rendered frame.
     * @param enable if true, depth buffering will be enabled
     */
    public void setDepthBufferEnable(boolean enable) {
        if (enable == depthBuffer) return;
        depthBuffer = enable;
        changeFlags |= CF_DEPTH_BUFFER;
    }

    // ----- Viewport and Projection -----

    /**
     * Returns the viewport width.
     * @return the current viewport width in pixels
     */
    public int getViewportWidth() {
        return vpWidth;
    }

    /**
     * Returns the viewport height.
     * @return the current viewport height in pixels
     */
    public int getViewportHeight() {
        return vpHeight;
    }

    /**
     * Returns the aspect ratio of the viewport.
     * @return the current aspect ratio
     */
    public float getViewportAspectRatio() {
        return aspect;
    }

    /**
     * Returns the vertical field of view.
     * @return the current vertical field of view in degrees
     */
    public float getFieldOfViewY() {
        return fovy;
    }

    /**
     * Sets a new vertical field of view.<p>
     * NOTE: this method call will force the recalculation of the current
     * projection matrix at the start of the next rendered frame if and only
     * if the current ProjectionPolicy is ProjectionPolicy.Perspective.
     * @param fovy the new vertical field of view
     */
    public void setFieldOfViewY(float fovy) {
        if (fovy == this.fovy) return;
        if (fovy <= 0.0f) throw new IllegalArgumentException();
        this.fovy = fovy;
        if (projection == ProjectionPolicy.Perspective) changeFlags |= CF_PROJECTION;
    }

    /**
     * Returns the near clipping distance.
     * @return the current near clipping distance
     */
    public float getNearClippingDistance() {
        return near;
    }

    /**
     * Sets a new near clipping distance.<p>
     * NOTE: this method call will force the recalculation of the current
     * projection matrix at the start of the next rendered frame.
     * @param near the new near clipping distance
     */
    public void setNearClippingDistance(float near) {
        if (near == this.near) return;
        this.near = near;
        changeFlags |= CF_PROJECTION;
    }

    /**
     * Returns the far clipping distance.
     * @return the current far clipping distance
     */
    public float getFarClippingDistance() {
        return far;
    }

    /**
     * Sets a new far clipping distance.<p>
     * NOTE: this method call will force the recalculation of the current
     * projection matrix at the start of the next rendered frame.
     * @param far the new far clipping distance
     */
    public void setFarClippingDistance(float far) {
        if (far == this.far) return;
        this.far = far;
        changeFlags |= CF_PROJECTION;
    }

    /**
     * Returns the left clipping coordinate.
     * @return the current left clipping coordinate
     */
    public float getLeftClipCoord() {
        return left;
    }

    /**
     * Returns the right clipping coordinate.
     * @return the current right clipping coordinate
     */
    public float getRightClipCoord() {
        return right;
    }

    /**
     * Returns the top clipping coordinate.
     * @return the current top clipping coordinate
     */
    public float getTopClipCoord() {
        return top;
    }

    /**
     * Returns the bottom clipping coordinate.
     * @return the current bottom clipping coordinate
     */
    public float getBottomClipCoord() {
        return bottom;
    }

    /**
     * Returns the scale factor.
     * @return the current scale factor
     */
    public float getScale() {
        return scale;
    }

    /**
     * Sets a new scale factor.<p>
     * NOTE: this method call will force the recalculation of the current
     * projection matrix at the start of the next rendered frame.
     * @param scale the new scale factor
     */
    public void setScale(float scale) {
        if (scale == this.scale) return;
        if (scale <= 0.0f) throw new IllegalArgumentException();
        this.scale = scale;
        changeFlags |= CF_PROJECTION;
    }

    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc=" Public Methods ">
    
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc=" Local Classes ">
    
    /**
     * Base class for the GraphicsContext3D singleton; provides access to the
     * current GL context.<p>
     * NOTE: This class should not used directly by the API user.
     */
    public static class GLContext3D {

        /** The internal GL object. */
        protected static GL2 gl;
        
        /** The internal GLU object. */
        protected static GLU glu;
        
        /** The internal delta time. */
        protected static float time;

        /**
         * Package private contructor for helper class.
         */
        protected GLContext3D() {}
        
        /**
         * Returns the internal GL object.
         * @return the current GL object
         */
        public GL2 getGL() {
            return gl;
        }
        
        /**
         * Returns the internal GLU object.
         * @return the current GLU object
         */
        public GLU getGLU() {
            return glu;
        }
        
        /**
         * Returns the delta time for the current frame.
         * @return the current delta time
         */
        public float getDeltaTime() {
            return time;
        }
        
    } // end class Context3D
    
    /**
     * An implementation of JOGL's GLEventListener interface.
     */
    private class GLListener extends GLContext3D implements GLEventListener {

        // ----- Private Fields -----
        
        /** The Canvas3D to which this view is attached. */
        private Canvas3D canvas;
    
        /** The default GL object, used for resetting the view to Release mode. */
        private GL2 releaseGL;

        /** The current GLAutoDrawable object. */
        private GLAutoDrawable drawable;

        /** The last nano timer value. */
        private long lastTime;
        
        // ----- Initialization -----
        
        GLListener(Canvas3D canvas) {
            this.canvas = canvas;
        }
        
        // ----- Implementation GLEventListener -----
        
        /**
         * Called by the drawable immediately after the OpenGL context is initialized.
         * @param drawable the GLCanvas object
         */
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        @Override
        public void init(GLAutoDrawable drawable) {
            // Initial code
            this.drawable = drawable;
            gl = (GL2) drawable.getGL();
            glu = new GLU();
            releaseGL = gl;
            if (mode != ViewModePolicy.Release) System.out.println("** init() **");
            // Check property flags
            if (changeFlags != 0) check();
            // Reset delta time
            lastTime = System.nanoTime();
            time = 0.0f;
            // Initialize render settings
            gl.glEnable(GL2.GL_TEXTURE_2D);
            frameCount = 1;
            // Notify Canvas3D
            canvas.init();
            // Reset gl object
            gl = null;
        }

        /**
         * Called by the drawable to initiate OpenGL rendering by the client.
         * @param drawable the GLCanvas object
         */
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        @Override
        public void display(GLAutoDrawable drawable) {
            // Initial code
            gl = (GL2) drawable.getGL();
            if (mode != ViewModePolicy.Release) {
                if (mode == ViewModePolicy.Trace || mode == ViewModePolicy.Debug && frameCount == 1) System.out.println("** display() **");
            }
            // Check change flags
            if (changeFlags != 0) check();
            // Get delta time
            long t = System.nanoTime();
            double delta = ((t - lastTime) / 1000000000.0);
            lastTime = t;
            time = (float) delta;
            // Initialize a new frame
            gl.glClear(clearFlags);
            gl.glLoadIdentity();
            frameCount++;
            // Notify Canvas3D
            canvas.preRender();
            canvas.render();
            canvas.postRender();
            // Reset gl object
            gl = null;
        }

        /**
         * Called by the drawable during the first repaint after the component
         * has been resized.
         * @param drawable the GLCanvas object
         * @param x the x coordinate of the new viewport position, usually zero
         * @param y the y coordinate of the new viewport position, usually zero
         * @param width the new viewport width
         * @param height the new viewport height
         */
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        @Override
        public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
            // Initial code
            gl = (GL2) drawable.getGL();
            if (mode != ViewModePolicy.Release) System.out.println("** reshape(" + width + ", " + height + ") **");
            // Check change flags
            if (changeFlags != 0) check();
            // Calculate new projection settings based on the new viewport size
            vpWidth = width;
            vpHeight = height == 0 ? 1 : height;
            aspect = (float) width / (float) height;
            float factor = (float) vpHeight * scale;
            right = aspect;
            top = 1;
            left = -right;
            bottom = -top;
            // Change projection
            changeProjection();
            // Do more tracing, if not in release mode
            if (mode != ViewModePolicy.Release) {
                System.out.println("  Fovy:   " + fovy);
                System.out.println("  Aspect: " + aspect);
                System.out.println("  Scale:  " + scale);
                System.out.println("  Left:   " + left);
                System.out.println("  Right:  " + right);
                System.out.println("  Bottom: " + bottom);
                System.out.println("  Top:    " + top);
                System.out.println("  Near:   " + near);
                System.out.println("  Far:    " + far);
            }
            // Notify Canvas3D
            canvas.reshape(width, height);
            // Reset gl object
            gl = null;
        }

        /**
         * Called by the drawable when the display mode or the display device
         * associated with the GLAutoDrawable has changed.
         * @param drawable the GLCanvas object
         * @param modeChanged indicates whether the display mode has changed
         * @param deviceChanged indicates whether the device has changed
         */
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        @Override
        public void dispose(GLAutoDrawable drawable) {
            // Initial code
            gl = (GL2) drawable.getGL();
            if (mode != ViewModePolicy.Release) System.out.println("** dispose() **");
            // Check cange flags
            if (changeFlags != 0) check();
            // Notify Canvas3D
            canvas.dispose();
            // Reset gl object
            gl = null;
        }

        // ----- Private Helper Methods -----

        /**
         * Check for changed view properties, if so, apply the changes.
         */
        private void check() {
            int cf = changeFlags;
            changeFlags = 0;
            if ((cf & CF_VIEW_MODE) != 0) changeMode();
            if ((cf & CF_CLEAR_COLOR) != 0) changeClearColor();
            if ((cf & CF_DEPTH_BUFFER) != 0) changeDepthBuffering();
            if ((cf & CF_PROJECTION) != 0) changeProjection();
        }

        /**
         * Changes the view mode policy.
         */
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        private void changeMode() {
            switch (mode) {
                case Release:
                    gl = releaseGL;
                    break;
                case Debug:
                    gl = new DebugGL2(releaseGL);
                    break;
                case Trace:
                    gl = new TraceGL2(releaseGL, System.out);
                    break;
            }
            drawable.setGL(gl);
        }

        /**
         * Changes the OpenGL background clear color.
         */
        private void changeClearColor() {
            if (color == null) clearFlags &= ~GL2.GL_COLOR_BUFFER_BIT;
            else {
                clearFlags |= GL2.GL_COLOR_BUFFER_BIT;
                gl.glClearColor(color.r, color.g, color.b, 0.0f);
            }
        }

        /**
         * Changes the OpenGL depth buffering flag.
         */
        private void changeDepthBuffering() {
            if (depthBuffer) {
                gl.glEnable(GL2.GL_DEPTH_TEST);
                clearFlags |= GL2.GL_DEPTH_BUFFER_BIT;
            }
            else {
                gl.glDisable(GL2.GL_DEPTH_TEST);
                clearFlags &= ~GL2.GL_DEPTH_BUFFER_BIT;
            }
        }

        /**
         * Changes the projection policy.
         */
        private void changeProjection() {
            gl.glMatrixMode(GL2.GL_PROJECTION);
            gl.glLoadIdentity();
            switch (projection) {
                case Perspective:
                    glu.gluPerspective(fovy / scale, aspect, near, far);
                    break;
                case Parallel:
                    gl.glOrtho(left / scale, right / scale, bottom / scale, top / scale, near, far);
                    break;
                case Frustum:
                    gl.glFrustum(left / scale, right / scale, bottom / scale, top / scale, near, far);
                    break;
            }
            gl.glMatrixMode(GL2.GL_MODELVIEW);
        }

    } // end class GLListener

    // </editor-fold>

} // end class View3D